﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Net;
using System.Web;
using System.Xml;
using log4net;
using Sherwood.Configuration;
using Sherwood.Web;

namespace Sherwood.Content.SearchServices
{
    public class SolrSearchHandler<TRecord> : IHttpHandler where TRecord : IRecord
    {
        private static readonly ILog Log = LogManager.GetLogger(typeof (SolrSearchHandler<>));

        private static readonly solrSearchConfiguration Config;
        private static readonly IRecordRepository<TRecord> RecordRepository;
        private readonly WebClient _webClient;

        static SolrSearchHandler()
        {
            if (Log.IsDebugEnabled)
                Log.Debug("Configuring " + typeof(SolrSearchHandler<>).FullName);

            var configRepository = DependencyInjection.Resolve<IConfigRepository>();
            using (TextReader reader = configRepository.CreateTextReader("SolrSearch.xml"))
                Config = solrSearchConfiguration.Load(reader);

            RecordRepository = DependencyInjection.Resolve<IRecordRepository<TRecord>>();
        }

        public SolrSearchHandler()
        {
            if (Log.IsDebugEnabled)
                Log.Debug("Creating new " + GetType().FullName);

            _webClient = new WebClient { BaseAddress = Config.server.url };
        }

        #region IHttpHandler Members

        public void ProcessRequest(HttpContext context)
        {
            context.Response.BufferOutput = false;
            try
            {
                using (var dataSession = DependencyInjection.Resolve<IDataSessionProvider>().CreateScope())
                {
                    if (!ProcessRequestInner(context))
                        return;

                    dataSession.Complete();
                }
            }
            catch (Exception exception)
            {
                Log.Error("Error", exception);
            }
        }

        private bool ProcessRequestInner(HttpContext context)
        {
            if (Log.IsDebugEnabled)
                Log.Debug("Processing " + context.Request.RawUrl);

            var queryString = new QueryParameterCollection(context.Request.RawUrl.Split('?')[1]);

            QueryParameter profile;
            string profileName = queryString.TryGetValue("profile", out profile) ? profile.Value : "default";

            var authenticator = SolrSearchProfileHelper<TRecord>.GetAuthenticator(profileName);
            if (!authenticator.Authenticates(queryString))
            {
                if (Log.IsWarnEnabled)
                    Log.Warn("Authentication failed on " + context.Request.RawUrl);
                context.Response.ContentType = "text/xml";
                context.Response.Write("<response><authenticationFailed/><result name=\"response\" numFound=\"0\" start=\"0\" /></response>");
                return false;
            }
            if(profile != null)
                queryString.Remove(profile);
            if (queryString.Count == 0)
            {
                context.Response.Write("<invalidUrl />");
                return false;
            }

            var injectors = SolrSearchProfileHelper<TRecord>.GetInjectors(profileName);
            foreach (var injector in injectors)
                injector.Prepare(queryString);

            if (Log.IsDebugEnabled)
                Log.Debug("Opening " + _webClient.BaseAddress + "select/" + queryString);

            Stream stream = _webClient.OpenRead("select/" + queryString);
            string contentType = _webClient.ResponseHeaders[HttpResponseHeader.ContentType];
            context.Response.ContentType = contentType;
            if (contentType.StartsWith("text/xml"))
            {
                WriteXmlStream(context, stream, injectors, RecordRepository);
            }
            else
            {
                var buffer = new byte[1024];
                int count;
                while ((count = stream.Read(buffer, 0, buffer.Length)) != 0)
                    context.Response.OutputStream.Write(buffer, 0, count);
            }

            return true;
        }

        public bool IsReusable
        {
            get { return true; }
        }

        #endregion

        private static void WriteXmlStream(HttpContext context, Stream stream,
                                           ICollection<ISolrXmlInjector<TRecord>> injectors,
                                           IRecordRepository<TRecord> recordRepository)
        {
            using (XmlReader reader = XmlReader.Create(stream))
            using (XmlWriter writer = XmlWriter.Create(context.Response.OutputStream))
            {
                if (injectors.Count == 0)
                {
                    // just write out the result directly - we do not need to alter anything
                    while (reader.Read())
                        reader.WriteNodeTo(writer);
                    return;
                }

                // since we will need all the records, we go through and find 50 records in each iteration to reduce db round trips
                // but first we write until we come to the result list
                while (reader.Read())
                {
                    bool isLast = reader.NodeType == XmlNodeType.Element && reader.Name == "result" && !reader.IsEmptyElement;
                    reader.WriteNodeTo(writer);
                    
                    if(isLast)
                        break;
                }

                while (!reader.EOF)
                {
                    var recordIds = new List<Guid>();

                    // now we read X records from solr and store the id. The xml stream is stored in a temporary stream
                    // so we can read it out again afterwards when we want to inject the additional data.
                    var ms = new MemoryStream();
                    var xmlWriterSettings = new XmlWriterSettings {ConformanceLevel = ConformanceLevel.Fragment};
                    using (var tempWriter = XmlWriter.Create(ms, xmlWriterSettings))
                    {
                        var counter = 0;

                        CopyXmlDocuments(reader, tempWriter,
                                          delegate(NameValueCollection values)
                                              {
                                                  var id = values["id"];
                                                  Guid recordId;
                                                  if (id != null && Guid.TryParse(id, out recordId))
                                                      recordIds.Add(recordId);
                                                  return ++counter <= Config.injection.recordBatchSize;
                                              });
                    }

                    // we prefetch the records and allow all injectors to do additional prefetching
                    Dictionary<Guid, TRecord> records = null;
                    var getRecordDictionary = new Func<Dictionary<Guid, TRecord>>(() => records ?? (records = recordRepository.GetByIds(recordIds).ToDictionary(r => r.Id)));
                    var getRecords = new Func<IEnumerable<TRecord>>(() => getRecordDictionary().Values);
                    
                    var prefetchResults = injectors.ToDictionary(
                        injector => injector,
                        injector =>
                            {
                                bool recordsAreNeeded;
                                var prefetch = injector.Prefetch(recordIds, getRecords, out recordsAreNeeded);
                                if (recordsAreNeeded)
                                    getRecordDictionary(); // ensure that we cache the records
                                return prefetch;
                            });

                    // we start reading out from the temporary stored xml to write it to our output stream
                    // while injecting extra data.
                    ms.Position = 0;
                    var xmlReaderSettings = new XmlReaderSettings {ConformanceLevel = ConformanceLevel.Fragment};
                    using (var tempReader = XmlReader.Create(ms, xmlReaderSettings))
                    {
                        CopyXmlDocuments(tempReader, writer,
                                          delegate(NameValueCollection values)
                                              {
                                                  var id = values["id"];
                                                  TRecord record = default(TRecord);
                                                  Guid recordId = Guid.Empty;
                                                  if (id != null && Guid.TryParse(id, out recordId) && records != null)
                                                      records.TryGetValue(recordId, out record);
                                                  
                                                  foreach (var injector in injectors)
                                                      injector.Inject(values, recordId, record, prefetchResults[injector], writer);

                                                  return true;
                                              });
                    }

                    if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "result")
                    {
                        // we are finished with the documents, so we just write out the rest
                        reader.WriteNodeTo(writer);
                        while (reader.Read())
                            reader.WriteNodeTo(writer);
                    }
                }
            }
        }

        private static void CopyXmlDocuments(XmlReader reader, XmlWriter writer, OnWriteDocumentEndTagHandler onWriteWriteDocumentEndTag)
        {
            bool continueReading = true;
            NameValueCollection values = null;
            while (continueReading && reader.Read())
            {
                if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "result")
                    // since we have reached the end of the result, we need to leave - there are no more documents
                    return;

                if (reader.NodeType == XmlNodeType.Element && reader.Name == "doc")
                    values = new NameValueCollection();
                if (reader.NodeType == XmlNodeType.Element && values != null && !reader.IsEmptyElement)
                {
                    string name = reader.GetAttribute("name");
                    if (!string.IsNullOrEmpty(name))
                    {
                        reader.WriteNodeTo(writer);
                        reader.Read();
                        values[name] = reader.Value;
                    }
                }
                else if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "doc" && values != null)
                {
                    continueReading = onWriteWriteDocumentEndTag(values);
                    values = null;
                }
                reader.WriteNodeTo(writer);
            }
        }

        private delegate bool OnWriteDocumentEndTagHandler(NameValueCollection values);
    }
}